feat(node): use diagnostics_channel for redis >= 5.12.0#20573
feat(node): use diagnostics_channel for redis >= 5.12.0#20573
Conversation
There was a problem hiding this comment.
Pull request overview
Adds diagnostics_channel-based Redis tracing for upcoming node-redis (>= 5.12.0) while narrowing the existing IITM patching to avoid double instrumentation once diagnostics events are available.
Changes:
- Restrict vendored node-redis IITM patcher to
>=5.0.0 <5.12.0. - Add a diagnostics_channel subscriber which creates Sentry spans from
node-redis:*tracing channels. - Ensure
@sentry/opentelemetry/*subpath imports stay external in the Node rollup build.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| packages/node/src/integrations/tracing/redis/vendored/redis-instrumentation.ts | Narrows the v5 IITM instrumentation version range to stop before 5.12.0. |
| packages/node/src/integrations/tracing/redis/redis-dc-subscriber.ts | New diagnostics_channel subscriber which creates/ends spans for command/batch/connect events and runs the responseHook. |
| packages/node/src/integrations/tracing/redis/index.ts | Wires the subscriber into Redis instrumentation (deferred to next tick). |
| packages/node/rollup.npm.config.mjs | Externalizes @sentry/opentelemetry/* imports so subpath entrypoints aren’t bundled. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // node-redis >= 5.12.0 publishes via diagnostics_channel. The subscriber uses | ||
| // `@sentry/opentelemetry/tracing-channel`, which needs the Sentry OTel context manager | ||
| // to be registered before it can `bindStore`. `initOpenTelemetry()` runs after integration | ||
| // `setupOnce`, so defer to the next tick. | ||
| Promise.resolve().then(() => subscribeRedisDiagnosticChannels(cacheResponseHook)); |
There was a problem hiding this comment.
The diagnostics_channel subscriber is deferred via a microtask. This means node-redis >=5.12.0 operations performed immediately after Sentry.init() (in the same synchronous tick) can run before the subscriber is attached and will not be traced. Consider hooking this up from the OpenTelemetry init path (after the context manager is registered) or otherwise ensuring subscription happens synchronously before user code continues.
| // node-redis >= 5.12.0 publishes via diagnostics_channel. The subscriber uses | |
| // `@sentry/opentelemetry/tracing-channel`, which needs the Sentry OTel context manager | |
| // to be registered before it can `bindStore`. `initOpenTelemetry()` runs after integration | |
| // `setupOnce`, so defer to the next tick. | |
| Promise.resolve().then(() => subscribeRedisDiagnosticChannels(cacheResponseHook)); | |
| // node-redis >= 5.12.0 publishes via diagnostics_channel. Subscribe synchronously so | |
| // operations performed immediately after `Sentry.init()` are traced without a race window. | |
| subscribeRedisDiagnosticChannels(cacheResponseHook); |
| // node-redis >= 5.12.0 publishes via diagnostics_channel. The subscriber uses | ||
| // `@sentry/opentelemetry/tracing-channel`, which needs the Sentry OTel context manager | ||
| // to be registered before it can `bindStore`. `initOpenTelemetry()` runs after integration | ||
| // `setupOnce`, so defer to the next tick. | ||
| Promise.resolve().then(() => subscribeRedisDiagnosticChannels(cacheResponseHook)); |
There was a problem hiding this comment.
This PR adds a new diagnostics_channel-based instrumentation path, but there are no unit tests covering span creation/end, error handling, or that the responseHook is invoked for command events. Since this package already has Redis tracing tests under packages/node/test/integrations/tracing/redis, it would be good to add a focused test suite for subscribeRedisDiagnosticChannels() using node:diagnostics_channel tracing channels.
size-limit report 📦
|
d150996 to
de731ed
Compare
node-redis 5.12.0 publishes command, batch, and connect events via node:diagnostics_channel. Subscribe to those channels via @sentry/opentelemetry/tracing-channel to produce spans without IITM- based monkey-patching, which lets the redis integration work on runtimes that don't support IITM (Bun, Deno, Cloudflare Workers). The existing OTel patcher is narrowed to '<5.12.0' so it does not double-instrument when both paths are present. The DC subscription is deferred to the next microtask so it runs after initOpenTelemetry() sets up the Sentry context manager (required for bindStore).
fdbf2b6 to
75744f4
Compare
75744f4 to
22acdcd
Compare
22acdcd to
38623e1
Compare
38623e1 to
f88ffb0
Compare
| error?: Error; | ||
| } | ||
|
|
||
| interface BatchData { |
There was a problem hiding this comment.
l/q: Are these types also vendored? If so it might be nicer if they live in the vendored folder
There was a problem hiding this comment.
Not vendored from OTEL. the those are exported by redis, shame we can't use it as a dev dep here.
| "prisma": "6.15.0", | ||
| "proxy": "^2.1.1", | ||
| "redis-4": "npm:redis@^4.6.14", | ||
| "redis-5": "npm:redis@^5.12.0", |
There was a problem hiding this comment.
m: I think it would make sense to add 2 tests. One for redis 5 with tracing channels and another without them. Then we would know if we break something.
There was a problem hiding this comment.
There are subtle differences between span counts with 5.12 and <5.12 as a result of some decisions in the redis tracing channel PR.
But yea we should have both to make sure we capture correctly.
| // `@sentry/opentelemetry/tracing-channel`, which needs the Sentry OTel context manager | ||
| // to be registered before it can `bindStore`. `initOpenTelemetry()` runs after integration | ||
| // `setupOnce`, so defer to the next tick. | ||
| void Promise.resolve().then(() => subscribeRedisDiagnosticChannels(cacheResponseHook)); |
There was a problem hiding this comment.
q: Would this work the same with CJS and ESM in Node? I wonder if there is a better way, than defer it to the next tick, like a "afterSetup" hook or so
There was a problem hiding this comment.
Yea, it works the same. We have an afterAllSetup but that could be much later than we like, or too early. I have not tried tbh.
Let me give afterAllSetup a go.
node-redis sanitizeArgs replaces MGET key arguments with '?' in the diagnostics_channel payload, so cache prefix detection cannot match — MGET remains a plain db.redis span.
| if (!span) return; | ||
| // Same slice: strip command name from args before passing to the response hook. | ||
| runResponseHook(span, data.command, data.args.slice(1), data.result); | ||
| span.end(); |
There was a problem hiding this comment.
Command error causes response hook on ended span
Medium Severity
Per the Node.js TracingChannel contract, when a traced promise rejects, both the error and asyncEnd events fire (in that order). The error handler sets the error status and ends the span, but then asyncEnd still runs runResponseHook on the already-ended span with a potentially undefined data.result, and calls span.end() again. The asyncEnd handler lacks a guard against the error-already-handled case, so cacheResponseHook may incorrectly set cache.hit: false (from calculateCacheItemSize(undefined) returning 0) on error spans.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 992feac. Configure here.
The hook unconditionally set origin to 'auto.db.otel.redis', clobbering the diagnostic_channel origin on all DC-sourced spans. Remove the override so each instrumentation path keeps its own origin.
…instrumentations Move origin attribute from the shared cacheResponseHook into the vendored OTel instrumentations so each path sets origin at span creation time, consistent with the DC subscriber.
| span.setStatus({ code: SPAN_STATUS_ERROR, message: data.error.message }); | ||
| } | ||
| span.end(); | ||
| }, |
There was a problem hiding this comment.
DC error handlers miss recordException unlike IITM path
Medium Severity
The error handlers in all three DC channel subscribers (command, batch, connect) set the span status but don't call span.recordException(data.error). The corresponding IITM vendored instrumentation error paths (e.g., _endSpanWithResponse and the connect .catch handler) all call span.recordException(error) before setting status. This means redis error spans from the DC path (>= 5.12.0) carry less diagnostic detail than equivalent spans from the IITM path (< 5.12.0).
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 5e6ea30. Configure here.
…nnect node-redis eagerly creates native TracingChannels on require(), and the DC subscriber is deferred via Promise.resolve().then(). Moving the require inside run() after a microtick ensures the subscriber is registered before the connect event fires.
ec090bb to
340e44a
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 340e44a. Configure here.
| // `@sentry/opentelemetry/tracing-channel`, which needs the Sentry OTel context manager | ||
| // to be registered before it can `bindStore`. `initOpenTelemetry()` runs after integration | ||
| // `setupOnce`, so defer to the next tick. | ||
| void Promise.resolve().then(() => subscribeRedisDiagnosticChannels(cacheResponseHook)); |
There was a problem hiding this comment.
Preload path permanently breaks DC context propagation
Medium Severity
When preloadOpenTelemetry is used (via --require), instrumentRedis() runs and schedules the DC subscription via Promise.resolve().then(). The microtask fires before the main module calls init(), so subscribeRedisDiagnosticChannels runs before the OTel context manager is set up. The tracingChannel helper falls back to returning the channel without bindStore (no context propagation), and sets subscribed = true. When init() later calls instrumentRedis() again (from setupOnce), the second microtask sees subscribed === true and exits early — the channels are never re-subscribed with proper context propagation. For redis >= 5.12 (where IITM is gated off), this results in orphaned spans without correct parent-child relationships.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 340e44a. Configure here.


Builds on #20510 and adds tracing channels subscribers via
node:diagnostics_channel.The module patchers are narrowed to
>=5.0.0 <5.12.0while the subscriber path runs unconditionally. it will be inert in older releases anyways and will activate when version 5.12 publishes the events.Verified that it works on Node and Bun equally as well, while IITM fails on Bun.